﻿/**************************************************************
 *
 * 唯一标识：a810b80a-0b2a-40d3-8a72-91d4ae2c6016
 * 命名空间：Sgr.Trackers.Infrastructure.Repositories
 * 创建时间：2024/7/17 18:36:44
 * 机器名称：DESKTOP-HJ4OAG9
 * 创建者：CocoYuan
 * 电子邮箱：fengqinhua2016@163.com
 * 描述：
 *
 **************************************************************/

using Microsoft.EntityFrameworkCore;
using Sgr.EntityFrameworkCore;
using Sgr.Trackers.Domain;
using System.Data;

namespace Sgr.Trackers.Infrastructure.Repositories
{
    /// <summary>
    /// 消息追踪日志仓储实现
    /// </summary>
    public class MessageTrackerLogRepository<TDbContext> : IMessageTrackerLogRepository where TDbContext : UnitOfWorkDbContext
    {
        private readonly IDbContextProvider<TDbContext> _dbContextProvider;

        public MessageTrackerLogRepository(IDbContextProvider<TDbContext> dbContextProvider)
        {
            _dbContextProvider = dbContextProvider ?? throw new ArgumentNullException(nameof(dbContextProvider));
        }

        /// <summary>
        /// 尝试将指定消息标记为“已处理”。
        /// 通过向幂等表 sgr_message_trackerlog 插入一条记录（主键为 sgr_id）实现幂等控制。
        ///
        /// 本方法不返回是否成功，而是在插入成功时表示该消息之前未处理；
        /// 如果插入失败（如唯一键冲突，表示该消息已处理过），将抛出 DbUpdateException。
        ///
        /// 通常，本方法应与业务逻辑一起包裹在同一个事务中，由调用方或事务机制统一处理异常与幂等逻辑。
        /// </summary>
        /// <param name="messageId">消息唯一标识，对应幂等表主键 sgr_id</param>
        /// <param name="cancellationToken">取消令牌</param>
        /// <returns>无返回值，是一个异步操作</returns>
        /// <exception cref="DbUpdateException">
        /// 当插入失败时可能抛出，例如由于唯一键冲突（消息已处理过）或其他数据库错误。
        /// </exception>
        public async Task MarkAsProcessedAsync(string messageId, CancellationToken cancellationToken)
        {
            var dbContext = await _dbContextProvider.GetDbContextAsync(cancellationToken);

            var processedOnUtc = DateTime.UtcNow;
            var sql = @"
        INSERT INTO sgr_message_trackerlog (sgr_id, m_processed_on_utc)
        VALUES (@messageId, @processedOnUtc)";

            await dbContext.Database.ExecuteSqlRawAsync(sql, new { messageId, processedOnUtc }, cancellationToken);
        }

        /// <summary>
        /// 清理早于指定时间的已处理消息记录，以控制表数据量，提升存储与查询效率。
        ///
        /// 本方法会查找所有 ProcessedOnUtc 字段早于 olderThanUtc 的记录，
        /// 按处理时间升序排序（最早处理的优先删除），并删除最多 max 条记录。
        ///
        /// 使用 EF Core 的 ExecuteDeleteAsync 实现高效批量删除，不查询实体到内存，
        /// 性能高，适合定期清理任务（如定时 Job）。
        /// </summary>
        /// <param name="olderThanUtc">
        /// 清理阈值时间（UTC）。所有 ProcessedOnUtc 字段值早于该时间的记录将被视为可清理。
        /// </param>
        /// <param name="max">
        /// 单次清理的最大记录条数，用于控制单次操作的影响范围，避免误删过多数据。
        /// 取值范围会被自动限制在 [1, 2000]，即：
        /// - 若 max 小于等于 0，则不执行任何清理，直接返回 0；
        /// - 若 max 大于 2000，则自动按 2000 处理。
        /// </param>
        /// <param name="cancellationToken">用于取消清理操作的令牌</param>
        /// <returns>
        /// 实际删除的记录条数。如果未执行删除（如参数无效或无符合条件的数据），则返回 0。
        /// </returns>
        public async Task<int> CleanupAsync(DateTime olderThanUtc, int max, CancellationToken cancellationToken = default)
        {
            // 如果最大清理条数小于等于 0，则不执行任何清理操作
            if (max <= 0)
            {
                return 0;
            }

            // 限制单次最多清理 2000 条，避免一次删除过多数据导致性能问题或锁表时间过长
            if (max > 2000)
            {
                max = 2000;
            }

            var db = await _dbContextProvider.GetDbContextAsync(cancellationToken);

            // 执行批量删除操作：
            // 1. 筛选条件：只选择已处理过的记录（ProcessedOnUtc 不为 null 且早于 olderThanUtc）
            // 2. 排序：按处理时间升序（最早处理的优先删除，有助于清理“老旧”数据）
            // 3. 限制：最多删除 max 条
            // 4. 执行：直接生成并执行 DELETE 语句，不查询实体到内存，性能高效
            var affected = await db.Set<MessageTrackerLog>()
                .Where(m => m.ProcessedOnUtc < olderThanUtc)
                .OrderBy(m => m.ProcessedOnUtc)
                .Take(max)
                .ExecuteDeleteAsync(cancellationToken);

            // 返回实际删除的记录条数
            return affected;
        }
    }
}